/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { ITypeScriptServiceClient } from '../typescriptService';
import { vsRangeToTsFileRange } from '../utils/convert';
import FormattingConfigurationManager from './formattingConfigurationManager';
import { getEditForCodeAction, applyCodeActionCommands } from '../utils/codeAction';
import { Command, CommandManager } from '../utils/commandManager';
import { createWorkspaceEditFromFileCodeEdits } from '../utils/workspaceEdit';
import DiagnosticsManager from './diagnostics';

class ApplyCodeActionCommand implements Command {
	public static readonly ID = '_typescript.applyCodeActionCommand';
	public readonly id = ApplyCodeActionCommand.ID;

	constructor(
		private readonly client: ITypeScriptServiceClient
	) { }

	public async execute(
		actions: any
	): Promise<boolean> {
		return applyCodeActionCommands(this.client, actions);
	}
}


class ApplyFixAllCodeAction implements Command {
	public static readonly ID = '_typescript.applyFixAllCodeAction';
	public readonly id = ApplyFixAllCodeAction.ID;

	constructor(
		private readonly client: ITypeScriptServiceClient,
		private readonly vscode: any
	) { }

	public async execute(
		file: string,
		tsAction: any,
	): Promise<void> {
		if (!tsAction.fixId) {
			return;
		}

		const args = {
			scope: {
				type: 'file',
				args: { file }
			},
			fixId: tsAction.fixId
		};

		try {
			const combinedCodeFixesResponse = await this.client.execute('getCombinedCodeFix', args);
			if (!combinedCodeFixesResponse.body) {
				return;
			}

			const edit = createWorkspaceEditFromFileCodeEdits(this.client, combinedCodeFixesResponse.body.changes, this.vscode);
			await this.vscode.workspace.applyEdit(edit);

			if (combinedCodeFixesResponse.command) {
				await this.vscode.commands.executeCommand(ApplyCodeActionCommand.ID, combinedCodeFixesResponse.command);
			}
		} catch {
			// noop
		}
	}
}

class SupportedCodeActionProvider {
	private _supportedCodeActions?: Promise<Set<any>>;

	public constructor(
		private readonly client: ITypeScriptServiceClient,
	) { }

	public async getFixableDiagnosticsForContext(context: any): Promise<any[]> {
		const supportedActions = await this.supportedCodeActions;
		return context.diagnostics.filter((diagnostic: any) => supportedActions.has(+diagnostic.code));
	}

	private get supportedCodeActions(): Promise<Set<number>> {
		if (!this._supportedCodeActions) {
			this._supportedCodeActions = this.client.execute('getSupportedCodeFixes', null, undefined)
				.then(response => response.body || [])
				.then(codes => codes.map((code: any) => +code).filter((code: any) => !isNaN(code)))
				.then(codes => new Set(codes));
		}
		return this._supportedCodeActions;
	}
}

export default class TypeScriptQuickFixProvider {

	private readonly supportedCodeActionProvider: SupportedCodeActionProvider;

	constructor(
		private readonly client: ITypeScriptServiceClient,
		private readonly formattingConfigurationManager: FormattingConfigurationManager,
		commandManager: CommandManager,
		private readonly diagnosticsManager: DiagnosticsManager,
		private readonly vscode: any
	) {
		commandManager.register(new ApplyCodeActionCommand(client));
		commandManager.register(new ApplyFixAllCodeAction(client, this.vscode));

		this.supportedCodeActionProvider = new SupportedCodeActionProvider(client);
	}

	public async provideCodeActions(
		document: any,
		_range: any,
		context: any,
		token: any
	): Promise<any[]> {
		if (!this.client.apiVersion.has213Features()) {
			return [];
		}

		const file = this.client.normalizePath(document.uri);
		if (!file) {
			return [];
		}

		const fixableDiagnostics = await this.supportedCodeActionProvider.getFixableDiagnosticsForContext(context);
		if (!fixableDiagnostics.length) {
			return [];
		}

		await this.formattingConfigurationManager.ensureFormatOptionsForDocument(document, token);

		const results: any[] = [];
		for (const diagnostic of fixableDiagnostics) {
			results.push(...await this.getFixesForDiagnostic(document, file, diagnostic, token));
		}
		return results;
	}

	private async getFixesForDiagnostic(
		document: any,
		file: string,
		diagnostic: any,
		token: any
	): Promise<Iterable<any>> {
		const args = {
			...vsRangeToTsFileRange(file, diagnostic.range),
			errorCodes: [+diagnostic.code]
		};
		const codeFixesResponse = await this.client.execute('getCodeFixes', args, token);
		if (codeFixesResponse.body) {
			const results: any[] = [];
			for (const tsCodeFix of codeFixesResponse.body) {
				results.push(...await this.getAllFixesForTsCodeAction(document, file, diagnostic, tsCodeFix));
			}
			return results;
		}
		return [];
	}

	private async getAllFixesForTsCodeAction(
		document: any,
		file: string,
		diagnostic: any,
		tsAction: any
	): Promise<Iterable<any>> {
		const singleFix = this.getSingleFixForTsCodeAction(diagnostic, tsAction);
		const fixAll = await this.getFixAllForTsCodeAction(document, file, diagnostic, tsAction);
		return fixAll ? [singleFix, fixAll] : [singleFix];
	}

	private getSingleFixForTsCodeAction(
		diagnostic: any,
		tsAction: any
	): any {
		const codeAction = new this.vscode.CodeAction(tsAction.description, this.vscode.CodeActionKind.QuickFix);
		codeAction.edit = getEditForCodeAction(this.client, tsAction, this.vscode);
		codeAction.diagnostics = [diagnostic];
		if (tsAction.commands) {
			codeAction.command = {
				command: ApplyCodeActionCommand.ID,
				arguments: [tsAction],
				title: tsAction.description
			};
		}
		return codeAction;
	}

	private async getFixAllForTsCodeAction(
		document: any,
		file: string,
		diagnostic: any,
		tsAction: any,
	): Promise<any | undefined> {
		if (!tsAction.fixId || !this.client.apiVersion.has270Features()) {
			return undefined;
		}

		// Make sure there are multiple diagnostics of the same type in the file
		if (!this.diagnosticsManager.getDiagnostics(document.uri).some(x => x.code === diagnostic.code && x !== diagnostic)) {
			return;
		}

		const action = new this.vscode.CodeAction(
			'{' + tsAction.description + '} (Fix all in file)',
			this.vscode.CodeActionKind.QuickFix);
		action.diagnostics = [diagnostic];

		action.command = {
			command: ApplyFixAllCodeAction.ID,
			arguments: [file, tsAction],
			title: ''
		};
		return action;
	}
}
